package ro.savatech.pg.flightlog;

import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Device {
	private static final Logger LOGGER = Logger.getLogger(Device.class
			.getName());

	private static final String CMD_READ_ID = "A0000BD";
	private static final String CMD_READ_FLIGHT_MEM = "A000086";
	private static final String CMD_READ_CFG_MEM = "A000085";

	private String portName;
	private SerialPort iqOnePort;
	private DataInputStream is;
	private PrintStream os;

	public Device() {

	}

	public void connectOnPort(String portName) {
		if (null != iqOnePort) {
			throw new DeviceAlreadyConnectedException();
		}

		this.portName = portName;
		CommPortIdentifier iqOnePortIdentifier;
		try {
			iqOnePortIdentifier = CommPortIdentifier
					.getPortIdentifier(this.portName);
			this.iqOnePort = (SerialPort) iqOnePortIdentifier.open(
					"flight manager", 30 * 1000);
			iqOnePort.setSerialPortParams(9600, SerialPort.DATABITS_8,
					SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
			is = new DataInputStream(iqOnePort.getInputStream());
			os = new PrintStream(iqOnePort.getOutputStream(), true);
		} catch (NoSuchPortException e) {
			throw new DeviceConnectionException("No such port.", e);
		} catch (PortInUseException e) {
			throw new DeviceConnectionException("The port is in use.", e);
		} catch (UnsupportedCommOperationException e) {
			throw new DeviceConnectionException(
					"Could not set port parameters.", e);
		} catch (IOException e) {
			throw new DeviceConnectionException("Could not open streams.", e);
		}
	}

	public void disconnect() {
		if (!isConnected()) {
			return;
		}
		try {
			this.is.close();
		} catch (IOException e) {
			LOGGER.log(Level.SEVERE, "Could not close input stream.", e);
		}
		this.os.close();

		this.iqOnePort.close();

		this.is = null;
		this.os = null;
		this.iqOnePort = null;
	}

	public DeviceIdentification readIdentification() {
		if (!this.isConnected()) {
			throw new DeviceNotConnectedException();
		}

		try {
			flushInput();
			sendCommand(CMD_READ_ID);
			BufferedReader reader = new BufferedReader(
					new InputStreamReader(is));
			String identificationLine = reader.readLine();

			return new DeviceIdentification(identificationLine);
		} catch (IOException e) {
			throw new DeviceConnectionException("Could not read data.", e);
		}
	}

	private void sendCommand(String cmd) {
		LOGGER.log(Level.INFO, "Sending: " + cmd);
		os.print(cmd);
		os.print("\r\n");
		os.flush();
	}

	private void flushInput() {
		try {
			int available = 0;
			while (0 != (available = this.is.available())) {
				LOGGER.info("Skipping " + available + " bytes.");
				this.is.skipBytes(available);
			}
		} catch (IOException e) {
			throw new DeviceConnectionException("Could not flush connection.",
					e);
		}
	}

	private boolean isConnected() {
		return null != this.iqOnePort;
	}

	/**
	 * Reads the entire device memory.
	 * 
	 * @return
	 */
	public ConfigurationMem readConfigurationMemory() {
		this.readIdentification();
		byte[] deviceMem = readRawConfigurationMemory();
		return ConfigurationMem.fromRaw(deviceMem);
	}

	private byte[] readRawConfigurationMemory() {
		byte deviceMem[] = new byte[ConfigurationMem.RECORD_SIZE];

		try {
			flushInput();
			sendCommand(CMD_READ_CFG_MEM);
			is.readFully(deviceMem);

		} catch (IOException e) {
			throw new DeviceConnectionException(
					"Could not read device configuration memory.", e);
		}
		return deviceMem;
	}

	public void dumpConfigurationMemory(OutputStream os) {
		this.readIdentification();
		byte[] deviceMem = readRawConfigurationMemory();
		try {
			os.write(deviceMem);
			os.flush();
		} catch (IOException e) {
			throw new FLException("Could not dump memory content.", e);
		}
	}

	public FlightMem readFlightMemory(FlightIndexEntry[] flightsIndex) {
		this.readIdentification();
		byte[] flightMem = readRawFlightMemory();
		return FlightMem.fromRaw(flightsIndex, flightMem);
	}

	private byte[] readRawFlightMemory() {
		byte flightMem[] = new byte[FlightMem.RECORD_SIZE];
		try {
			flushInput();
			sendCommand(CMD_READ_FLIGHT_MEM);
			is.readFully(flightMem);
		} catch (IOException e) {
			throw new DeviceConnectionException(
					"Could not read device flight memory.", e);
		}
		return flightMem;
	}

	public void dumpFlightMemory(OutputStream os) {
		this.readIdentification();
		byte[] flightMem = readRawFlightMemory();
		try {
			os.write(flightMem);
			os.flush();
		} catch (IOException e) {
			throw new FLException("Could not dump memory content.", e);
		}
	}
}
